//Copyright 2011 - Jonathan Allen

using System;
using System.Linq;
using System.Reflection;
using Granite.DataAnnotations;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace Granite.Metadata
{
	/// <summary>
	/// Cached Metadata about the indicated type.
	/// </summary>
	[Pure]
	public class ClassMetadata
	{
		readonly PropertyMetadataCollection m_Properties = new PropertyMetadataCollection();

		[Pure]
		internal ClassMetadata(Type type)
		{
			Contract.Requires(type != null);

			var shadowingProperties = (from p in type.GetProperties() where IsHidingMember(p) select p).ToList();

			foreach (var propertyInfo in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
			{
				Contract.Assume(propertyInfo != null);

				var isHidden = !shadowingProperties.Contains(propertyInfo) && shadowingProperties.Any(p => p.Name == propertyInfo.Name);

				//TODO - Properly support shadowed properties.
				if (isHidden)
					continue;

				m_Properties.Add(new PropertyMetadata(propertyInfo));
			}

			//List the properties that are affected when the indicated property is modified.
			foreach (var property in m_Properties)
			{
				Contract.Assume(property != null);
				foreach (CalculatedFieldAttribute fieldList in property.PropertyInfo.GetCustomAttributes(typeof(CalculatedFieldAttribute), true))
				{
					Contract.Assume(fieldList != null);
					foreach (var field in fieldList.SourceProperties)
					{

						Contract.Assume(field != null);
						if (!m_Properties.Contains(field))
							throw new InvalidOperationException("Cannot find property " + field + " on type " + type.FullName + ". This is needed for the calculated property " + property.Name);

						m_Properties[field].AddCalculatedField(property);
					}
				}
			}

		}

		static bool IsHidingMember(PropertyInfo propertyInfo)
		{
			Contract.Requires(propertyInfo != null);
			Contract.Assume(propertyInfo.DeclaringType != null);

			var baseType = propertyInfo.DeclaringType.BaseType;
			if (baseType == null)
				return false; //can thhis ever happen?

			var baseProperty = baseType.GetProperty(propertyInfo.Name);

			if (baseProperty == null)
				return false;

			if (baseProperty.DeclaringType == propertyInfo.DeclaringType)
				return false; //can thhis ever happen?

			//TODO: Properly support write-only properties
			if (baseProperty.GetGetMethod() == null || propertyInfo.GetGetMethod() == null)
				throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Cannot support classes with write-only properties. Class: {0} PropertyName {1}", propertyInfo.DeclaringType.Name, propertyInfo.Name));


			var basePropertyGetGetMethod = baseProperty.GetGetMethod();
			var propertyInfoGetGetMethod = propertyInfo.GetGetMethod();
			Contract.Assume(basePropertyGetGetMethod != null);
			Contract.Assume(propertyInfoGetGetMethod != null);

			var baseMethodDefinition = basePropertyGetGetMethod.GetBaseDefinition();
			var thisMethodDefinition = propertyInfoGetGetMethod.GetBaseDefinition();
			Contract.Assume(baseMethodDefinition != null);
			Contract.Assume(thisMethodDefinition != null);


			return baseMethodDefinition.DeclaringType != thisMethodDefinition.DeclaringType;
		}

		/// <summary>
		/// Properties on the indicated class
		/// </summary>
		public PropertyMetadataCollection Properties
		{
			get
			{
				Contract.Ensures(Contract.Result<PropertyMetadataCollection>() != null);
				return m_Properties;
			}
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
		[ContractInvariantMethod]
		void ObjectInvaiant()
		{
			Contract.Invariant(m_Properties != null);
			Contract.Invariant(Contract.ForAll(m_Properties, p => p != null));
		}

	}
}
